#include "Filesystem.h"
#include "muduo/base/Logging.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <list>
#include <sstream>
#include <sys/types.h>
#include <pwd.h>
#include <vector>
#include <errno.h>
#include <grp.h>
#include <chrono>
#include <dirent.h>
#include <iomanip>


using namespace ftp;
using namespace ftp::Filesystem;

const char* FileStatus::kDefaultRealOwnerString = "Easy-Ftp";
const char* FileStatus::kDefaultRealGroupString = "Easy-Ftp";

FileStatus::FileStatus(const std::string& localPath)
	: path_(localPath),
	maskRealOwner_(false)
{
	const int res = stat(localPath.c_str(), &fileStatus_);
	// Check file status by the return value of stat()
	isOk_ = (res == 0);
}

FileStatus::~FileStatus() = default;

bool FileStatus::isOk() const
{
	return isOk_;
}

FileType FileStatus::type() const
{
	FileType res = FileType::Unknown;
	if (!isOk_)
		return res;

	switch (fileStatus_.st_mode & S_IFMT) {
	case S_IFREG: // regular file
		res = FileType::RegularFile;
		break;
	case S_IFDIR: // directory
		res = FileType::Dir;
		break;
	case S_IFCHR: // character device
		res = FileType::CharacterDevice;
		break;
	case S_IFBLK: // block device
		res = FileType::BlockDevice;
		break;
	case S_IFLNK: // symlink
		res = FileType::SymbolicLink;
		break;
	case S_IFIFO: // FIFO/pipe
		res = FileType::Fifo;
		break;
	case S_IFSOCK: // socket
		res = FileType::Socket;
		break;
	default:
		break;
	}

	return res;
}

// @brief Return file size
// 
// Return type long is 64bit on 64-bit platform, 32bit on 32-bit platform.
// Define macro __USE_FILE_OFFSET64 on 32-bit platform if real file size > 4GB
// 
// @return file size
// 
int64_t FileStatus::fileSize() const
{
	if (!isOk_)
		return 0;

	return fileStatus_.st_size;
}

//---------------------------------------------------------
// Get File permission

// Note: UNIX V7  (and  later  systems)  had  S_IREAD,  S_IWRITE,  S_IEXEC, 
// where POSIX prescribes the synonyms S_IRUSR, S_IWUSR, S_IXUSR.

bool FileStatus::permissionOwnerRead()    const { return (fileStatus_.st_mode & S_IRUSR) != 0; }
bool FileStatus::permissionOwnerWrite()   const { return (fileStatus_.st_mode & S_IWUSR) != 0; }
bool FileStatus::permissionOwnerExecute() const { return (fileStatus_.st_mode & S_IXUSR) != 0; }
bool FileStatus::permissionGroupRead()    const { return (fileStatus_.st_mode & S_IRGRP) != 0; }
bool FileStatus::permissionGroupWrite()   const { return (fileStatus_.st_mode & S_IWGRP) != 0; }
bool FileStatus::permissionGroupExecute() const { return (fileStatus_.st_mode & S_IXGRP) != 0; }
bool FileStatus::permissionOtherRead()    const { return (fileStatus_.st_mode & S_IROTH) != 0; }
bool FileStatus::permissionOtherWrite()   const { return (fileStatus_.st_mode & S_IWOTH) != 0; }
bool FileStatus::permissionOtherExecute() const { return (fileStatus_.st_mode & S_IXOTH) != 0; }


std::string FileStatus::permissionString() const
{
	std::string permissionStr(9, '-');
	if (!isOk_)
		return permissionStr;
	// Owner
	permissionStr[0] = (fileStatus_.st_mode & S_IRUSR) ? 'r' : '-';
	permissionStr[1] = (fileStatus_.st_mode & S_IWUSR) ? 'w' : '-';
	permissionStr[2] = (fileStatus_.st_mode & S_IXUSR) ? 'x' : '-';
	// Group
	permissionStr[3] = (fileStatus_.st_mode & S_IRGRP) ? 'r' : '-';
	permissionStr[4] = (fileStatus_.st_mode & S_IWGRP) ? 'w' : '-';
	permissionStr[5] = (fileStatus_.st_mode & S_IXGRP) ? 'x' : '-';
	// Other
	permissionStr[6] = (fileStatus_.st_mode & S_IROTH) ? 'r' : '-';
	permissionStr[7] = (fileStatus_.st_mode & S_IWOTH) ? 'w' : '-';
	permissionStr[8] = (fileStatus_.st_mode & S_IXOTH) ? 'x' : '-';

	return permissionStr;
}

bool FileStatus::maskRealOwner() const
{
	return maskRealOwner_;
}

std::string FileStatus::ownerString() const
{
	if (!isOk_ || maskRealOwner_)
		return kDefaultRealOwnerString;
	else
		return getRealOwnerString();
}

std::string FileStatus::groupString() const
{
	if (!isOk_ || maskRealOwner_)
		return kDefaultRealGroupString;
	else
		return getRealGroupString();
}

std::string FileStatus::timeString() const
{
	if (!isOk_)
		return "Jan 1 1970"; // default time

	// The Ftp date format is either :
	//
	//      MMM DD hh : mm
	//    OR
	//      MMM DD  YYYY
	//    OR
	//      MMM DD YYYY
	// 
	// If current time is the same year with file time, we can choose case 1, i.e. don't return year.
	// 
	// https://files.stairways.com/other/ftp-list-specs-info.txt

	auto now = std::chrono::system_clock::now();
	time_t now_time_t = std::chrono::system_clock::to_time_t(now); // time_point to time_t

	struct tm nowTm{};
	struct tm fileTm{};

	localtime_r(&now_time_t, &nowTm);
	localtime_r(&fileStatus_.st_ctime, &fileTm);

	int curYear = nowTm.tm_year;
	int fileYear = fileTm.tm_year;

	static const std::string monthNames[] = {
		"Jan",
		"Feb",
		"Mar",
		"Apr",
		"May",
		"Jun",
		"Jul",
		"Aug",
		"Sep",
		"Oct",
		"Nov",
		"Dec"
	};

	std::stringstream ssDateTime;
	if (curYear == fileYear) {
		// Return time rather than year
		ssDateTime << std::setw(3) << fileTm.tm_mday << " "
			<< std::setw(2) << std::setfill('0') << fileTm.tm_hour << ":"
			<< std::setw(2) << std::setfill('0') << fileTm.tm_min;
	}
	else {
		// Do not return time, only date
		static constexpr int kBaseYear = 1900;
		ssDateTime << std::setw(3) << fileTm.tm_mday
			<< "  " << (fileTm.tm_year + kBaseYear);
	}

	return monthNames[fileTm.tm_mon] + ssDateTime.str();
}

bool FileStatus::canOpenDir() const
{
	if (!isOk_)
		return false;

	if (type() != FileType::Dir)
		return false;

	// Check if the file path_ can be opened as a directory
	bool res = false;
	DIR* dp;
	if ((dp = opendir(path_.c_str())) != NULL) {
		res = true;
	}
	closedir(dp);

	return res;
}

std::string FileStatus::getRealOwnerString() const
{
	struct passwd pwd;
	struct passwd* resPwd;
	long bufSize;
	std::string ownerStr;

	// get buffer max size
	if ((bufSize = sysconf(_SC_GETPW_R_SIZE_MAX)) == -1) {
		bufSize = 16384; // guess
	}

	// get password file entry to get owner's name by uid
	char* buf = static_cast<char*>(malloc(sizeof(char) * static_cast<unsigned long>(bufSize)));
	int savedErrno = errno;
	errno = 0; // clear errno before call getpwuid_r()
	int ret = getpwuid_r(fileStatus_.st_uid, &pwd, buf, static_cast<size_t>(bufSize), &resPwd);
	if (ret != 0) { // error
		LOG_SYSERR << "getFileOwner getpwuid_r error";
		ownerStr = kDefaultRealOwnerString;
	}
	else {
		ownerStr = pwd.pw_name;
	}
	errno = savedErrno;
	free(buf);

	return ownerStr;
}

std::string FileStatus::getRealGroupString() const
{
	struct group grp;
	struct group* resGrp;
	long bufSize;
	std::string groupStr;

	// get buffer max size
	if ((bufSize = sysconf(_SC_GETGR_R_SIZE_MAX)) == -1) {
		bufSize = 16384; // guess
	}

	// get password file entry to get owner's name by uid
	char* buf = static_cast<char*>(malloc(sizeof(char) * static_cast<unsigned long>(bufSize)));
	int savedErrno = errno;
	errno = 0; // clear errno before call getpwuid_r()
	int ret = getgrgid_r(fileStatus_.st_gid, &grp, buf, static_cast<size_t>(bufSize), &resGrp);
	if (ret != 0) { // error
		LOG_SYSERR << "getFileGroup getgrgid_r error";
		groupStr = kDefaultRealGroupString;
	}
	else {
		groupStr = grp.gr_name;
	}
	errno = savedErrno;
	free(buf);

	return groupStr;
}

bool FileStatus::isFile() const
{

    return S_ISREG(fileStatus_.st_mode) != 0;
}

bool FileStatus::isDir() const
{
    return S_ISDIR(fileStatus_.st_mode) != 0;
}

std::map<std::string, FileStatus> Filesystem::dirContent(const std::string& path)
{
	std::map<std::string, FileStatus> content; // file name, file status
	DIR* dp;
	struct dirent* dirp;
	if ((dp = opendir(path.c_str())) == NULL) { // error
		return content;
	}

	// Traverse every sub-file of directory path specified
	while ((dirp = readdir(dp)) != NULL) {
		content.emplace(std::string(dirp->d_name), Filesystem::FileStatus(path + '/' + std::string(dirp->d_name)));
	}
	closedir(dp);
	return content;
}

// @brief Clean segment ".", ".." of the path by trying to remove "." and ".." in the path.
// 
// Why need to clean path?
// Because more than one "." and ".." will disturb the path analysis.
// 
// @param path: absolute path, or relative path
// 
// Example 1: We can resolve the only one ".."
//   Primary path:
//     "./User/.././User/../"
//   Clean path(Unix/Linux):
//      "."
// 
// Example 2: We can resolve the only one ".."
//   Primary path:
//     "/User/.././User/../"
//   Clean path(Unix/Linux):
//     "/"
// 
// Example 3: We cannot resolve the two ".."
//   Primary path:
//     "../.."
//   Clean path:
//     "../.."
//
std::string Filesystem::simplifyPath(const std::string& path, char outputSeparator)
{
	enum class PathType { kRelativePath = 0, kAbsolutePath };

	const std::string curRelativePath = ".";
	if (path.empty() || path == ".")
		return curRelativePath; // default path

	PathType pathType;
	// Find the root for absolute path
	std::string absoluteRootPath;
	// On Unix/Linux, root path is '/'
	if (path[0] == '/') { // absolute path
		absoluteRootPath = '/';
		pathType = PathType::kAbsolutePath;
	}
	else { // relative path
		pathType = PathType::kRelativePath;
	}

	// Split the path
	std::list<std::string> components; // every element is a path component, not including separator
	if (path.size() >= absoluteRootPath.size() + 1) {
		size_t start;
		if (absoluteRootPath.empty()) // path is relative path
			start = 0;
		else // path is absolute path, include a "/", need to ignore
			start = absoluteRootPath.size();

		do {
			size_t end = path.find_first_of("/", start); // find from path[start, ...), any char is ok

			std::string curComponent;
			if (end == std::string::npos)
				curComponent = path.substr(start); // path[start, ...)
			else
				curComponent = path.substr(start, end - start); // path[start, end)

			if (curComponent.empty() || curComponent == ".") {// ignore "" and "."
			}
			else if (curComponent == "..") { // components will shrink when current component is ".."

				if (pathType == PathType::kAbsolutePath) { // absolute path
					if (!components.empty()) {
						// move to parent directory by removing "..", 
						// if we do not stay at root directory now
						components.pop_back();
					}
					else {
						// We have reached root directory now, no need to remove "..", 
						// so ignore this case
					}
				}
				else { // relative path
					if (!components.empty() && (components.back() != "..")) {
						// move to parent directory if we can resolve previous component
						components.pop_back();
					}
					else {
						// We can not resolve previous component, 
						// as it is empty or ".."
						components.push_back(curComponent);
					}
				}
			}
			else {
				components.push_back(curComponent);
			}

			if (end == std::string::npos) // there is no path segment left to split
				break;
			start = end + 1; // update start to next position

		} while (start < path.size());
	}

	// param path is relative path, and all components are removed.
	// so return a default path.
	if (components.empty() && pathType == PathType::kRelativePath) {
		return curRelativePath;
	}

	// package all path components from components to a new path
	std::stringstream ss;
	ss << absoluteRootPath;
	for (auto it = components.begin(); it != components.end(); ++it) {
		if (it != components.begin())
			ss << outputSeparator;

		ss << *it;
	}

	return ss.str();
}

std::string Filesystem::simplifyNativePath(const std::string& path)
{
	return simplifyPath(path, '/');
}
